import EventEmitter from "events";
import { Script, createContext } from 'vm';

import { Kline, Indicator, Signal } from "../lib/quant";
import { MergeData } from "./DataHandler";
import { debugLogger, errLogger, outLogger } from "../common/logger";
import { Logger } from "log4js";
import { CustomIndicators, MixIndicator } from "./MixIndicator";
import { Statistics, sample } from "./Statistics";
import { PERIOD_INTERVAL_MAP, removeTypeFromFunction } from "../utils";
import { omit } from "lodash";
import dayjs from "dayjs";
import { MixIndicatorsTolerance } from "./types";
import { Period } from "./PeriodProcess";
import { isSimilarTo } from "./util";


export interface SymbolOrderConfig {
  stopLoss: number;
  stopProfit: number;
  /** 贪婪 */
  greedy: boolean;
}

export type CustomStrategieContext = {
  debugLogger: Logger['info'];
  strategy: CustomStrategie,
  sample?: Statistics,
  isSimilarTo: typeof isSimilarTo,
  config: {
    mode: number,
    mixIndicatorsTolerance: MixIndicatorsTolerance
  }
}
type Indicators = Map<string, Indicator<Kline>>;
type CustomStrategie = (kline: MergeData, indicators: Indicators, mixIndicator: CustomIndicators) => Signal | [Signal, any];

export interface CustomStrategy {
  id: string;
  symbol: string;
  strategy: CustomStrategie;
  config: {
    mode: number,
    similarity_target?: number,
    period?: Period,
    mixIndicatorsTolerance: MixIndicatorsTolerance
  }
  code?: string;
  _listener?: (...arg: any[]) => void;
}

interface StrategyOptions {
  /** 交易限制(时间：秒)，避免出现高频 */
  tradeFrequencyLimit?: number,
  isMock?: boolean
}




const defaultOptions: Partial<StrategyOptions> = {
  tradeFrequencyLimit: PERIOD_INTERVAL_MAP['30m'] * 2,
  isMock: false,
}
/** 
 * 交易策略
 * 包含数据指标 + 策略执行
 */
export class TradingStrategy {
  private eventEmitter = new EventEmitter();
  private customStrategies: CustomStrategy[] = [];
  private symbolsMixIndicator: Map<string, MixIndicator> = new Map();
  private PRE_TRADE_TIME: Record<string, number> = {};
  private options: StrategyOptions;

  constructor(tradingStrategyOptions: StrategyOptions = {}) {
    this.options = Object.assign({}, defaultOptions, tradingStrategyOptions);
  }
  addSymbol(symbol: string) {
    if (this.symbolsMixIndicator.has(symbol)) {
      return;
    }

    const mixIndicator = new MixIndicator({isMock: this.options.isMock});
    mixIndicator.onMixIndicator(this.executeStrategy);
    this.symbolsMixIndicator.set(symbol, mixIndicator);
  }
  removeSymbol(symbol: string) {
    if (this.symbolsMixIndicator.get(symbol)) {
      this.symbolsMixIndicator.get(symbol).destroy();
      this.symbolsMixIndicator.delete(symbol);
    }
    this.removeCustomStrategy({symbol});
  }
  addCustomStrategy(id: string, symbol: string, options: {
    code?: string, 
  }) {
    const mergedContext: CustomStrategieContext = {
      debugLogger: (...arg) => {
        outLogger.info(...arg)
      },
      strategy: undefined,
      config: undefined,
      sample,
      isSimilarTo,
    };
    try {
      const script = new Script(options.code || '');
      const context = createContext(mergedContext);
      script.runInContext(context);
      Object.assign(mergedContext, context);
    } catch (error) {
      throw Error(error);
    }
    const target = this.customStrategies.find(s => s.symbol === symbol && s.id === id);

    const newCustomStrategy = {
      id,
      symbol,
      strategy: mergedContext.strategy,
      config: mergedContext.config,
      _listener: undefined
    }
    let has = true
    if (!target) {
      has = false;
      this.customStrategies.push(newCustomStrategy);
    } else {
      outLogger.info(`addCustomStrategy  ${symbol} 已存在, strategy_id: ${id}`);
    }
    return {
      onSignal: (callback: (signal: Signal, kline: MergeData & {label_text?: string}) => void) => {
        if (!has) {
          this.eventEmitter.on(`strategy:${id}:${symbol}`, callback);
          newCustomStrategy._listener = callback;
        }
      }
    }
  }

  udpateCustomStrategy(id: string, symbol: string, options: {
    code?: string, 
  }): {
    symbol: string;
    strategy: CustomStrategie;
  } {
    const mergedContext: CustomStrategieContext = {
      debugLogger: (...arg) => {
        outLogger.info(...arg)
      },
      strategy: undefined,
      config: undefined,
      sample,
      isSimilarTo,
    };
    const target = this.customStrategies.find(s => {
      return s.symbol === symbol && s.id === id
    });
    
    if (target && options.code) {
      const script = new Script(options.code);
      const context = createContext(mergedContext);
      script.runInContext(context);
      Object.assign(mergedContext, context);
      target.strategy = mergedContext.strategy;
      target.config = mergedContext.config
    }
    outLogger.info(`udpateCustomStrategy strategy_id: ${id} ${symbol} find:${Boolean(target)} len: ${this.customStrategies.length}`);
    return target;
  }

  removeCustomStrategy({id, symbol}: {id?: string, symbol?: string}): void {
    for (let i = 0; i < this.customStrategies.length; i++) {
      const customStrategy = this.customStrategies[i];
      const key = `strategy:${customStrategy.id}:${customStrategy.symbol}`
      if (id && symbol) {
        if (customStrategy.symbol === symbol && customStrategy.id === id) {
          this.customStrategies.splice(i, 1);
          this.eventEmitter.removeListener(key, customStrategy._listener);
          return;
        }
      } else if (id && customStrategy.id === id) {
        this.customStrategies.splice(i, 1);
        this.eventEmitter.removeListener(key, customStrategy._listener);
        i--;
      } else if (symbol && customStrategy.symbol === symbol) {
        this.customStrategies.splice(i, 1);
        this.eventEmitter.removeListener(key, customStrategy._listener);
        i--;
      }
    }
  }

  getCustomStrategy(id: string, symbol: string) {
    return this.customStrategies.find(s => {
      return s.symbol === symbol && s.id === id;
    });
  }

  onMixIndicator(callback: (kline: MergeData, mixIndicator: MixIndicatorsTolerance) => void) {
    this.eventEmitter.on('mix-indicator', callback);
  }
  executeStrategy = (kline: MergeData, indicators: Indicators, mixIndicator: MixIndicatorsTolerance): void => {
  
    if (!this.options.isMock && kline.isMock) {
      return
    }
    this.eventEmitter.emit('mix-indicator', kline, mixIndicator);
    // 执行用户自定义策略
    for (const customStrategy of this.customStrategies) {
      if (customStrategy.symbol === kline.symbol) {
        const key = `${customStrategy.id}-${kline.symbol}`;
        if (this.orderOver(key, kline.timestamp)) {
          continue
        } 
        let signal: Signal;
        let label_text = '';
        let mixIndicatorsTolerance = customStrategy.config.mixIndicatorsTolerance
   
        const vector_stort = mixIndicator[`vector_stort_${customStrategy.config.period}`] as number[] ?? []
        // 避免执行错误
        try {
   
          if (customStrategy.config.mode == 0 && mixIndicatorsTolerance !== undefined) {

            const sampleBar = sample.vectorSimilarityCunstom(
              kline, 
              '4h',
              Statistics.jsonToVectors(mixIndicator), 
              Statistics.jsonToVectors(mixIndicatorsTolerance), 
              0
            );
            if (!sampleBar || sampleBar.label === 0) {
              continue
            }

            signal = sampleBar.label === -1 ? 'SELL' : 'BUY'
            label_text = `${signal}: ${dayjs(sampleBar.time).format('YY/M/D H:m:sZ')}`;
       
          } else if (customStrategy.config.mode == 1) {
            signal = customStrategy.strategy(kline, indicators, mixIndicator) as Signal;
          } else if (customStrategy.config.mode === 3) {
            const sampleBar = sample.vectorSimilarityManhattan(
              kline, 
              customStrategy.config.period,
              vector_stort, 
              customStrategy.config.similarity_target
            );
            if (!sampleBar || sampleBar.label === 0 
              || (sampleBar.label === -1 && (mixIndicator.change_rate_15m < 0 || mixIndicator.close_ma_rate_15m < 0))
              || (sampleBar.label === 1 && (mixIndicator.change_rate_15m > 0 || mixIndicator.close_ma_rate_15m > 0))
            ) {
              continue
            }
            signal = sampleBar.label === -1 ? 'SELL' : 'BUY'
            label_text = `${signal}: ${dayjs(sampleBar.time).format('YY/M/D H:m:sZ')}`;
          } else if (customStrategy.config.mode === undefined ) {
            const [s, sampleBar] = customStrategy.strategy(kline, indicators, mixIndicator) as [Signal, any];
            signal = s;
            label_text = sampleBar ? `${signal}: ${dayjs(sampleBar.time).format('YY/M/D H:m:sZ')}` : '';
          }
        } catch (error) {
          errLogger.info('executeStrategy', error.message);
        }
        // debugLogger(`executeStrategy ${customStrategy.id} ${customStrategy.symbol} signal: ${signal} mode: ${customStrategy.config.mode}`, true)
        if (signal) {
          this.eventEmitter.emit(`strategy:${customStrategy.id}:${customStrategy.symbol}`, signal, Object.assign(kline, { label_text }));
          this.PRE_TRADE_TIME[key] = kline.timestamp;
        }
      }
    }
    
  }

  /** 时间范围内超出交易频率 */
  orderOver(key: string, timestamp: number) {
    return this.PRE_TRADE_TIME[key] !== undefined 
    && this.options.tradeFrequencyLimit > 0 
    && (timestamp - this.PRE_TRADE_TIME[key] < this.options.tradeFrequencyLimit)
  }

  push(data: MergeData, period: keyof MixIndicator['quantMap']) {
    this.addSymbol(data.symbol);
    this.symbolsMixIndicator.get(data.symbol).push(data, period);
  }
  update(data: MergeData, period: keyof MixIndicator['quantMap']) {
    this.addSymbol(data.symbol);
    this.symbolsMixIndicator.get(data.symbol).update(data, period)
  }
  destroy() {
    this.eventEmitter.removeAllListeners();
    this.customStrategies = null;
    this.PRE_TRADE_TIME = null;
    this.symbolsMixIndicator.clear();
    this.symbolsMixIndicator = null;
    this.options = null;
  }
}



export function defaultStrategy (config = {
  mode: 1, 
  mixIndicatorsTolerance: {},
  similarity_target: 2.12,
} as any) {
  function strategy (kline: MergeData, indicators: Indicators, mixIndicator: CustomIndicators) {
    // const ma120_15m = indicators.get('ma120');
    const { 
      close_ma_rate_15m,
      close_ma_rate_4h,
      exceptional_undulation_rate_4h,
      volume_rate_15m,
      volume_rate_4h,
      point_peaks_15m,
      point_valleys_15m,
      slope_15m,
    } = mixIndicator;
    const positive = kline.close > kline.open;
    const close_ma_target = 0.03;
    const exceptional_undulation_4h_target = 0.1;
    if (
      close_ma_rate_15m > close_ma_target &&
      point_peaks_15m >= 2 &&
      volume_rate_4h > 0.1 &&
      (slope_15m === 1 || slope_15m === 3) &&
      volume_rate_15m > 0.08 &&
      close_ma_rate_4h > close_ma_target * 2 &&
      exceptional_undulation_rate_4h > exceptional_undulation_4h_target &&
      positive
    ) {
      return "SELL";
    } else if (
      close_ma_rate_15m < -close_ma_target &&
      point_valleys_15m >= 2 &&
      volume_rate_4h > 0.1 &&
      (slope_15m === -1 || slope_15m === -3) &&
      volume_rate_15m > 0.08 &&
      close_ma_rate_4h < -(close_ma_target * 2) &&
      exceptional_undulation_rate_4h > exceptional_undulation_4h_target &&
      !positive
    ) {
      return "BUY";
    }
  }
  return `
${removeTypeFromFunction(strategy.toString())}
var config = ${JSON.stringify(config, null, 2)}`
}

